﻿/*=========================================================================
 * AUTH: Matthew Baum, 9753095
 * DESC: Game Level
 *=========================================================================*/

using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace RisingThreat
{
    class Level
    {
        /*================================================================
         * Expected Level Dimension Constants
         * 
         * NOTE: offsets have "-1" because we are counting from zero,
         * not one when looping through input level file.
         *================================================================*/ 
        private static int LEVEL_WIDTH;
        private static int LEVEL_DEPTH;
        private static int LEVEL_MAX_TILE_COUNT;
        private static int LEVEL_WIDTH_OFFSET;
        private static int LEVEL_DEPTH_OFFSET; 

        // Accessors
        public static int getWidth()
        {
            return LEVEL_WIDTH;
        }
        public static int getDepth()
        {
            return LEVEL_DEPTH;
        }

        // Path Constants
        private const string LEVEL_PATH_PREFIX = "LEVEL/level_";
        private const string LEVEL_PATH_SUFFIX = ".txt";

        // Static counter for level number
        private static short LEVEL_COUNTER = 0;

        // Floor tiles
        private List<LevelTile> floor;

        // Walls
        private List<LevelWall> walls;

        // Obstacles
        private List<CubicObstacle> obstacles = new List<CubicObstacle>(20);

        // Start Tile
        private StartTile start;

        // Goal Tile
        private GoalTile goal;

        // Enemy Spawn Points
        private List<SpawnPoint> spawnPoints = new List<SpawnPoint>();

        // Graphics device reference
        private GraphicsDevice graphicsDevice;

        // Quad tree BVH leaf node load factor
        private static readonly int BVH_LEAF_LOAD = 3;

        // Quad Tree BVH
        private QuadPartition bvh;

        // Level graph
        public LevelGraph graph;

        // Textures
        private Texture2D wallTexture;
        private Texture2D floorTexture;
        private Texture2D crateTexture;

        // Basic Effects
        private BasicEffect wallEffect;
        private BasicEffect floorEffect;
        private BasicEffect crateEffect;

        // CONSTRUCTOR
        public Level(GraphicsDeviceManager graphics, ContentManager Content, Matrix view, Matrix proj, Model mod, List<Teammate> teammates, Player player)
        {
            // Set graphics device
            graphicsDevice = graphics.GraphicsDevice;

            // INIT
            init(graphics, Content, view, proj, mod, teammates, player);
        }

        // INIT the level
        private void init(GraphicsDeviceManager graphics, ContentManager Content, Matrix view, Matrix proj, Model mod, List<Teammate> teammates, Player player)
        {
            // Measure level file, and init level dimensions
            measureLevelFile();

            // INIT path-finding graph
            graph = new LevelGraph(Content, graphics, LEVEL_WIDTH, LEVEL_DEPTH);

            // Load data into the level structures
            loadLevelData(mod, teammates, player);

            // Load level content
            loadLevelContent(Content);

            // Define graphics effects
            defineLevelEffects(graphics, view, proj);
        }

        // RE-INIT the level (do NOT reload content)
        private void reinit(GraphicsDeviceManager graphics, ContentManager Content, Matrix view, Matrix proj, Model mod, List<Teammate> teammates, Player player)
        {
            // Measure level file, and init level dimensions
            measureLevelFile();

            // INIT path-finding graph
            graph = new LevelGraph(Content, graphics, LEVEL_WIDTH, LEVEL_DEPTH);

            // Load data into the level structures
            loadLevelData(mod, teammates, player);
        }

        // Get potentially colliding items from the BVH Quad Tree
        public HashSet<QuadTreeItem> getPotentialCollisions(BoundingSphere boundingsphere)
        {
            return bvh.getPotentialCollisions(boundingsphere);
        }

        // Get intersecting items (for parallel rays) from BVH
        public HashSet<KeyValuePair<QuadTreeItem, float?>> getRayIntersections(Ray ray)
        {
            return bvh.getRayIntersections(ray);
        }

        // Calculate level file width (X : columns) and depth (Z : lines)
        private void measureLevelFile()
        {
            // Construct level file path
            string fPath = LEVEL_PATH_PREFIX + LEVEL_COUNTER.ToString() + LEVEL_PATH_SUFFIX;

            // Get file lines
            String[] lines = File.ReadAllLines(fPath);

            // Get column count on first line (width), assume lines are same size
            int colCount = lines[0].Length;

            // Get line count (depth)
            int lineCount = lines.GetLength(0);

            // Set level dimension info
            LEVEL_WIDTH = colCount;
            LEVEL_DEPTH = lineCount;
            LEVEL_MAX_TILE_COUNT = LEVEL_WIDTH * LEVEL_DEPTH;
            LEVEL_WIDTH_OFFSET = LEVEL_WIDTH / 2 - 1;
            LEVEL_DEPTH_OFFSET = LEVEL_DEPTH / 2 - 1;

            // Init level lists
            floor = new List<LevelTile>(LEVEL_MAX_TILE_COUNT / 2);
            walls = new List<LevelWall>(LEVEL_MAX_TILE_COUNT / 2);

            // Init BVH size
            bvh = new QuadPartition
                (
                    null, 
                    Vector3.Zero,
                    new Vector3
                        (
                            LEVEL_WIDTH * LevelTile.TILE_WIDTH, 
                            LevelWall.WALL_HEIGHT * 2, 
                            LEVEL_DEPTH * LevelTile.TILE_DEPTH
                        ),
                    BVH_LEAF_LOAD
                );
        }

        // Load level content
        private void loadLevelContent(ContentManager Content)
        {
            // Load textures
            wallTexture = Content.Load<Texture2D>("textures/plaster");
            floorTexture = Content.Load<Texture2D>("textures/floor");
            crateTexture = Content.Load<Texture2D>("textures/crate");
        }

        // Define graphics effects
        private void defineLevelEffects(GraphicsDeviceManager graphics, Matrix view, Matrix proj)
        {
            // Define graphics effect for walls
            wallEffect = new BasicEffect(graphics.GraphicsDevice);
            wallEffect.EnableDefaultLighting();
            wallEffect.World = Matrix.Identity;
            wallEffect.View = view;
            wallEffect.Projection = proj;
            wallEffect.TextureEnabled = true;
            wallEffect.Texture = wallTexture;

            // Define graphics effect for floors
            floorEffect = new BasicEffect(graphics.GraphicsDevice);
            floorEffect.EnableDefaultLighting();
            floorEffect.World = Matrix.Identity;
            floorEffect.View = view;
            floorEffect.Projection = proj;
            floorEffect.TextureEnabled = true;
            floorEffect.Texture = floorTexture;

            // Define graphics effect for crates (obstacles)
            crateEffect = new BasicEffect(graphics.GraphicsDevice);
            crateEffect.EnableDefaultLighting();
            crateEffect.World = Matrix.Identity;
            crateEffect.View = view;
            crateEffect.Projection = proj;
            crateEffect.TextureEnabled = true;
            crateEffect.Texture = crateTexture;
        }

        // Update
        public void update(Matrix view, Matrix proj, TimeSpan elapsedTime, Player player)
        {
            wallEffect.View = view;
            wallEffect.Projection = proj;

            floorEffect.View = view;
            floorEffect.Projection = proj;

            crateEffect.View = view;
            crateEffect.Projection = proj;

            // Update spawn points
            updateSpawnPoints(elapsedTime, player);
        }

        // Update spawn points
        private void updateSpawnPoints(TimeSpan elapsedTime, Player player)
        {
            foreach (SpawnPoint spawn in spawnPoints)
            {
                spawn.update(elapsedTime, player);
            }
        }

        // Reset spawn points
        public void resetSpawnPoints()
        {
            foreach (SpawnPoint spawn in spawnPoints)
            {
                spawn.reset();
            }
        }

        // Load level data from a text file
        private void loadLevelData(Model mod, List<Teammate> teammates, Player player)
        {
            // Construct level file path
            string fPath = LEVEL_PATH_PREFIX + LEVEL_COUNTER.ToString() + LEVEL_PATH_SUFFIX;

            // Open file
            StreamReader read = new StreamReader(fPath);

            // Read file line-by-line, and process each line
            string lineData;

            // While file has data
            for(int lineNumber = 0; read.Peek() != -1; ++lineNumber)
            {
                // Read line
                lineData = read.ReadLine();

                // Process line
                loadDataFromLine(lineNumber, lineData, mod, teammates, player);
            }

            // GRAPH: Generate edges!
            graph.generateEdges();
        }

        // Create level geometry from a Line of Level Data
        private void loadDataFromLine(int lineNumber, string lineData, Model mod, List<Teammate> teammates, Player player)
        {
            // Iterate over the characters in the line, and process each one
            for(int columnNumber = 0; columnNumber != lineData.Length; ++ columnNumber)
            {
                // Process the current character
                loadDataFromCharacter(lineNumber, columnNumber, lineData[columnNumber], mod, teammates, player);
            }
        }

        // Create level geometry for a single character of parsed level data
        private void loadDataFromCharacter(int lineNumber, int columnNumber, char value, Model mod, List<Teammate> teammates, Player player)
        {
            // Calculate X and Z offsets, in terms of tiles
            int xOffset = columnNumber - LEVEL_WIDTH_OFFSET;
            int zOffset = lineNumber - LEVEL_DEPTH_OFFSET;

            // SWITCH based on input value
            switch(value)
            {
                // EMPTY SPACE
                case '.':

                break;

                // BARE FLOOR TILE
                case '0':

                    // Create new floor tile
                    floor.Add( new LevelTile(xOffset, zOffset, graphicsDevice) );

                    // Set new path-finding graph node
                    addGraphNode(xOffset, zOffset, value, lineNumber, columnNumber);

                break;

                // TOP WALL
                case '1':
                // LEFT WALL
                case '2':
                // BOTTOM WALL
                case '3':
                // RIGHT WALL
                case '4':

                    // Create floor tile
                    floor.Add( new LevelTile(xOffset, zOffset, graphicsDevice) );

                    // Create new wall
                    LevelWall wall = new LevelWall(xOffset, zOffset, value, graphicsDevice);
                    walls.Add(wall);

                    // Add wall to BVH
                    bvh.insert(wall);

                    // Set new path-finding graph node
                    addGraphNode(xOffset, zOffset, value, lineNumber, columnNumber);

                break;

                // CORNER 1
                case '5':

                    // Create floor tile
                    floor.Add( new LevelTile(xOffset, zOffset, graphicsDevice) );

                    // Create "top" and "left" walls
                    createTwoWalls(xOffset, zOffset, '1', '2');

                    // Set new path-finding graph node
                    addGraphNode(xOffset, zOffset, value, lineNumber, columnNumber);

                    break;

                // CORNER 2
                case '6':

                    // Create floor tile
                    floor.Add( new LevelTile(xOffset, zOffset, graphicsDevice) );

                    // Create "left" and "bottom" walls
                    createTwoWalls(xOffset, zOffset, '2', '3');

                    // Set new path-finding graph node
                    addGraphNode(xOffset, zOffset, value, lineNumber, columnNumber);

                    break;

                // CORNER 3
                case '7':

                    // Create floor tile
                    floor.Add( new LevelTile(xOffset, zOffset, graphicsDevice) );

                    // Create "bottom" and "right" walls
                    createTwoWalls(xOffset, zOffset, '3', '4');

                    // Set new path-finding graph node
                    addGraphNode(xOffset, zOffset, value, lineNumber, columnNumber);

                    break;

                // CORNER 4
                case '8':

                    // Create floor tile
                    floor.Add( new LevelTile(xOffset, zOffset, graphicsDevice) );

                    // Create "right" and "top" walls
                    createTwoWalls(xOffset, zOffset, '4', '1');

                    // Set new path-finding graph node
                    addGraphNode(xOffset, zOffset, value, lineNumber, columnNumber);

                    break;

                // PIPE 1
                case '9':

                    // Create floor tile
                    floor.Add( new LevelTile(xOffset, zOffset, graphicsDevice) );

                    // Create "left" and "right" walls
                    createTwoWalls(xOffset, zOffset, '2', '4');

                    // Set new path-finding graph node
                    addGraphNode(xOffset, zOffset, value, lineNumber, columnNumber);

                    break;

                // PIPE 2
                case 'A':

                    // Create floor tile
                    floor.Add( new LevelTile(xOffset, zOffset, graphicsDevice) );

                    // Create "top" and "bottom" walls
                    createTwoWalls(xOffset, zOffset, '1', '3');

                    // Set new path-finding graph node
                    addGraphNode(xOffset, zOffset, value, lineNumber, columnNumber);

                    break;

                // END 1
                case 'B':

                    // Create floor tile
                    floor.Add( new LevelTile(xOffset, zOffset, graphicsDevice) );

                    // Create "top", "left" and "right" walls
                    createThreeWalls(xOffset, zOffset, '1', '2', '4');

                    // Set new path-finding graph node
                    addGraphNode(xOffset, zOffset, value, lineNumber, columnNumber);

                    break;

                // END 2
                case 'C':

                    // Create floor tile
                    floor.Add( new LevelTile(xOffset, zOffset, graphicsDevice) );

                    // Create "top", "left" and "bottom" walls
                    createThreeWalls(xOffset, zOffset, '1', '2', '3');

                    // Set new path-finding graph node
                    addGraphNode(xOffset, zOffset, value, lineNumber, columnNumber);

                    break;

                // END 3
                case 'D':

                    // Create floor tile
                    floor.Add( new LevelTile(xOffset, zOffset, graphicsDevice) );

                    // Create "left", "bottom" and "right" walls
                    createThreeWalls(xOffset, zOffset, '2', '3', '4');

                    // Set new path-finding graph node
                    addGraphNode(xOffset, zOffset, value, lineNumber, columnNumber);

                    break;

                // END 4
                case 'E':

                    // Create floor tile
                    floor.Add( new LevelTile(xOffset, zOffset, graphicsDevice) );

                    // Create "bottom", "right" and "top" walls
                    createThreeWalls(xOffset, zOffset, '3', '4', '1');

                    // Set new path-finding graph node
                    addGraphNode(xOffset, zOffset, value, lineNumber, columnNumber);

                    break;

                // OBSTACLE
                case 'X':

                    // Create new obstacle
                    CubicObstacle obstacle = new CubicObstacle(xOffset, zOffset, graphicsDevice);

                    // Add to obstacle list
                    obstacles.Add(obstacle);

                    // Add to BVH
                    bvh.insert(obstacle);

                break;

                // START tile
                case 'S':

                    // Create start tile
                    start = new StartTile(xOffset, zOffset, graphicsDevice);

                    // Add to floor
                    floor.Add(start);

                    // Set new path-finding graph node
                    addGraphNode(xOffset, zOffset, value, lineNumber, columnNumber);

                    break;

                // GOAL tile
                case 'G':

                    // Create goal tile
                    goal = new GoalTile(xOffset, zOffset, graphicsDevice);

                    // Add to floor
                    floor.Add(goal);

                    // Set new path-finding graph node
                    addGraphNode(xOffset, zOffset, value, lineNumber, columnNumber);

                    break;

                // NEST (enemy spawn point) tile
                case 'N':

                    // Create floor tile
                    floor.Add( new LevelTile(xOffset, zOffset, graphicsDevice) );

                    // Assume we do not want to path-find accross a nest ...

                    // Create a new nest/spawn point
                    spawnPoints.Add
                    (
                        new SpawnPoint
                        (
                            xOffset * LevelTile.TILE_DEPTH, 
                            zOffset * LevelTile.TILE_DEPTH,
                            mod, 
                            teammates, 
                            player, 
                            graph
                        )
                    );

                    break;

                // DEFAULT : Invalid value
                default:
                break;
            }
        }

        // Private helper to set a path-finding graph node
        private void addGraphNode(int xOffset, int zOffset, char type, int lineNumber, int columnNumber)
        {
            // Set new path-finding graph node
            graph.addNode
            (
                new LevelGraphNode
                (
                    xOffset * LevelTile.TILE_DEPTH, 
                    zOffset * LevelTile.TILE_DEPTH,
                    type
                ),
                lineNumber,
                columnNumber
            );
        }

        // Private helper to create two walls
        private void createTwoWalls(int xOffset, int zOffset, char type1, char type2)
        {
            // New wall item
            LevelWall wall;

            // Create first wall
            wall = new LevelWall(xOffset, zOffset, type1, graphicsDevice);

            // Add to walls list
            walls.Add(wall);

            // Add to BVH
            bvh.insert(wall);

            // Create second wall
            wall = new LevelWall(xOffset, zOffset, type2, graphicsDevice);

            // Add to walls list
            walls.Add(wall);

            // Add to BVH
            bvh.insert(wall);
        }

        // Private helper to create three walls
        private void createThreeWalls(int xOffset, int zOffset, char type1, char type2, char type3)
        {
            // New wall item
            LevelWall wall;

            // Create first wall
            wall = new LevelWall(xOffset, zOffset, type1, graphicsDevice);

            // Add to walls list
            walls.Add(wall);

            // Add to BVH
            bvh.insert(wall);

            // Create second wall
            wall = new LevelWall(xOffset, zOffset, type2, graphicsDevice);

            // Add to walls list
            walls.Add(wall);

            // Add to BVH
            bvh.insert(wall);

            // Create third wall
            wall = new LevelWall(xOffset, zOffset, type3, graphicsDevice);

            // Add to walls list
            walls.Add(wall);

            // Add to BVH
            bvh.insert(wall);
        }

        // Draw the Level
        public void draw(GraphicsDeviceManager graphics, Effect effect, Matrix view, Matrix proj)
        {
            // Draw floor
            foreach(LevelTile tile in floor)
            {
                if(tile is GoalTile)
                {
                    tile.draw(graphics, effect);
                }
                else
                {
                    tile.draw(graphics, floorEffect);
                }
            }

            // Draw walls
            foreach (LevelWall wall in walls)
            {
                wall.draw(graphics, wallEffect);
            }

            // Draw obstacles
            foreach(CubicObstacle obstacle in obstacles)
            {
                obstacle.draw(graphics, crateEffect);
            }

            // DEBUG: draw pathfinding graph
            //graph.draw(view, proj, effect);
        }

        // For curiosity, get BVH height
        public int getBVHHeight()
        {
            return bvh.getHeight();
        }

        // For curiosity, get BVH total count
        public int getBVHCount()
        {
            return bvh.getCount();
        }

        // Get level counter
        public static short getLevelCounter()
        {
            return LEVEL_COUNTER;
        }

        // PRIVATE HELPER to clear level data
        private void clearLevel()
        {
            // Prevents a memory leak, but incurs runtime computation
            /*foreach (LevelTile tile in floor)
            {
                tile.dispose();
            }

            foreach (LevelWall wall in walls)
            {
                wall.dispose();
            }*/

            // Clear level
            floor.Clear();
            walls.Clear();
            obstacles.Clear();
            spawnPoints.Clear();
            graph = null;

            // Force garbage collection here
            GC.Collect();
        }

        // Funtion to load the next level into an existing level object
        public void loadNextLevel(GraphicsDeviceManager graphics, ContentManager Content, Matrix view, Matrix proj, Model mod, List<Teammate> teammates, Player player)
        {
            // Clear level data
            clearLevel();

            // Increment level counter
            ++LEVEL_COUNTER;

            // Construct level file path
            string fPath = LEVEL_PATH_PREFIX + LEVEL_COUNTER.ToString() + LEVEL_PATH_SUFFIX;

            // Cycle back to level 0, IF we have reached the last level
            if(File.Exists(fPath) == false)
            {
                LEVEL_COUNTER = 0;
            }

            // HACK
            LevelGraphNode.ID_COUNTER = 0;

            // Re-Init the level
            reinit(graphics, Content, view, proj, mod, teammates, player);
        }

        public LevelTile getStartTile()
        {
            return start;
        }

        public LevelTile getGoalTile()
        {
            return goal;
        }

        // Get all enemies from the spawn points
        public List<Enemy> getEnemies()
        {
            // Create a new list
            List<Enemy> enemies = new List<Enemy>(spawnPoints.Count * SpawnPoint.MAX_SPAWN_COUNT);

            // Populate the list
            foreach (SpawnPoint spawn in spawnPoints)
            {
                enemies.AddRange(spawn.getEnemies());
            }

            // Return the list
            return enemies;
        }
    }
}